﻿using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Centido.Core.Dto;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Centido.Core.Test
{
    /// <summary>
    ///   Data transfer object generic testing.
    /// </summary>
    [TestClass]
    public abstract class GenericDtoTest
    {
        /// <summary>
        ///   Retrieves the name of the data transfer object xml tag.
        /// </summary>
        /// <returns></returns>
        protected abstract string GetXmlTag();


        /// <summary>
        ///   Retrives the current xml schema version.
        /// </summary>
        /// <returns></returns>
        protected abstract string GetCurrentXmlSchemaVersion();


        /// <summary>
        ///   Creates a new data transfer object.
        /// </summary>
        /// <returns></returns>
        protected abstract DataTransferObject CreateDto();


        /// <summary>
        ///   Retrieves the list of data transfer object fields.
        /// </summary>
        /// <returns></returns>
        protected abstract IEnumerable<DtoFieldInfo> GetFields();


        /// <summary>
        ///   Copies the properties of the data transfer object to the corresponding object.
        /// </summary>
        /// <param name="dto">The data transfer object.</param>
        /// <returns>Returns the corresponding object after copying.</returns>
        protected abstract object CopyFrom(DataTransferObject dto);


        /// <summary>
        ///   Creates a data transfer object from the correpsonding object.
        /// </summary>
        /// <param name="dto">The created data transfer object.</param>
        /// <param name="initialObject">The initial object.</param>
        protected abstract void CreateDtoFromObject(out DataTransferObject dto, out object initialObject);


        /// <summary>
        ///   Sets the object property value.
        /// </summary>
        /// <param name="targetObject">The target object.</param>
        /// <param name="propertyName">The name of the property.</param>
        /// <param name="propertyValue">The value of the property.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="targetObject"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="propertyName"/> is null.</exception>
        /// <exception cref="ArgumentException">Thrown when the value of <paramref name="propertyName"/> is an empty string.</exception>
        /// <exception cref="ArgumentException">Thrown when the property with the specified name was not found.</exception>
        protected void SetProperty(object targetObject, string propertyName, object propertyValue)
        {
            if (targetObject == null)
                throw new ArgumentNullException("targetObject");
            if (propertyName == null)
                throw new ArgumentNullException("propertyName");
            if (string.IsNullOrEmpty(propertyName))
                throw new ArgumentException("The property name cannot be empty.");

            var property = targetObject.GetType().GetProperties().FirstOrDefault(p => p.Name == propertyName);

            if (property == null) throw new ArgumentException("The property with the specified name was not found.");

            property.SetValue(targetObject, propertyValue, null);
        }


        /// <summary>
        ///   Gets the property value of the specified object.
        /// </summary>
        /// <param name="targetObject">The specified object.</param>
        /// <param name="propertyName">The property name.</param>
        /// <returns>Returns the property value of the specified object.</returns>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="targetObject"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="propertyName"/> is null.</exception>
        /// <exception cref="ArgumentException">Thrown when the value of <paramref name="propertyName"/> is an empty string.</exception>
        /// <exception cref="ArgumentException">Thrown when the property with the specified name was not found.</exception>
        protected object GetProperty(object targetObject, string propertyName)
        {
            if (targetObject == null)
                throw new ArgumentNullException("targetObject");
            if (propertyName == null)
                throw new ArgumentNullException("propertyName");
            if (string.IsNullOrEmpty(propertyName))
                throw new ArgumentException("The property name cannot be empty.");

            var property = targetObject.GetType().GetProperties().FirstOrDefault(p => p.Name == propertyName);

            if (property == null) throw new ArgumentException("The property with the specified name was not found.");

            var value = property.GetValue(targetObject, null);

            return value;
        }


        /// <summary>
        ///   Checks that the two values are equal.
        /// </summary>
        /// <param name="firstValue">The first value.</param>
        /// <param name="secondValue">The second value.</param>
        /// <returns>Returns true if the two values are equal.</returns>
        private bool AreEqual(object firstValue, object secondValue)
        {
            if (firstValue == null && secondValue == null)
                return true;

            if (firstValue == null || secondValue == null)
                return false;

            if (firstValue is DateTime && secondValue is DateTime)
            {
                var firstDate = (DateTime)firstValue;
                var secondDate = (DateTime)secondValue;

                return firstDate.Date == secondDate.Date
                    && firstDate.Hour == secondDate.Hour
                    && firstDate.Minute == secondDate.Minute;
            }

            // If the first value is enumeration
            if (firstValue is Enum)
                firstValue = (int)firstValue;

            // If the second value is enumeration
            if (secondValue is Enum)
                secondValue = (int)secondValue;

            return firstValue.Equals(secondValue);
        }


        /// <summary>
        ///   Converts the value to string.
        /// </summary>
        /// <param name="value">The specified value.</param>
        /// <exception cref="ArgumentNullException">Throwm when the value of <paramref name="value"/> is null.</exception>
        /// <returns>Returns the corresponding string.</returns>
        private string ToString(object value)
        {
            if (value == null)
                throw new ArgumentNullException("value");

            if (value is DateTime)
                return ((DateTime)value).ToString(CultureInfo.InvariantCulture);

            if (value is decimal)
                return ((decimal)value).ToString(CultureInfo.InvariantCulture);

            return value.ToString();
        }


        /// <summary>
        ///   Passing null stream when reading from xml.
        /// </summary>
        [TestMethod]
        [ExpectedException(typeof(ArgumentNullException))]
        public void PassingNullStreamWhenReadingFromXml()
        {
            var dto = CreateDto();

            dto.ReadXml(null);
        }


        /// <summary>
        ///   Reading from xml specifying all attributes.
        /// </summary>
        [TestMethod]
        public void ReadingFromXmlWithAllAttributes()
        {
            var fieldList = GetFields().ToList();

            // Encoding the first dto
            var xml = string.Format("<root><{0} {1}=\"{2}\" ",
                GetXmlTag(),
                DataTransferObject.XmlSchemaVersionTag,
                GetCurrentXmlSchemaVersion());

            xml = fieldList.Aggregate(xml,
                (current, fieldInfo) =>
                    current + string.Format("{0}=\"{1}\" ", fieldInfo.XmlName,
                    ToString(fieldInfo.SampleValue)));

            xml += "/>";

            // Encoding the second dto
            xml += string.Format("<{0} {1}=\"{2}\" ",
                GetXmlTag(),
                DataTransferObject.XmlSchemaVersionTag,
                GetCurrentXmlSchemaVersion());

            xml = fieldList.Aggregate(xml,
                (current, fieldInfo) =>
                    current + string.Format("{0}=\"{1}\" ",
                    fieldInfo.XmlName, ToString(fieldInfo.SampleValue)));

            xml += string.Format("></{0}></root>", GetXmlTag());

            var firstDto = CreateDto();
            var secondDto = CreateDto();

            var stream = new XmlTextReader(new MemoryStream(Encoding.UTF8.GetBytes(xml)));

            stream.ReadStartElement();

            firstDto.ReadXml(stream);
            secondDto.ReadXml(stream);

            foreach (var fieldInfo in fieldList)
                Assert.IsTrue(AreEqual(fieldInfo.SampleValue, GetProperty(firstDto, fieldInfo.Name)));

            foreach (var fieldInfo in fieldList)
                Assert.IsTrue(AreEqual(fieldInfo.SampleValue, GetProperty(secondDto, fieldInfo.Name)));
        }


        /// <summary>
        ///   Reading from xml specifying the minimum set of attributes.
        /// </summary>
        [TestMethod]
        public void ReadingFromXmlWithMinAttributes()
        {
            var fieldList = GetFields().Where(f => f.IsRequired).ToList();

            // Encoding the first dto
            var xml = string.Format("<root><{0} ",
                GetXmlTag());

            xml = fieldList.Aggregate(xml,
                (current, fieldInfo) =>
                    current + string.Format("{0}=\"{1}\" ",
                    fieldInfo.XmlName, ToString(fieldInfo.SampleValue)));

            xml += string.Format("></{0}>", GetXmlTag());

            // Encoding the second dto
            xml += string.Format("<{0} ",
                GetXmlTag());

            xml = fieldList.Aggregate(xml,
                (current, fieldInfo) =>
                    current + string.Format("{0}=\"{1}\" ",
                    fieldInfo.XmlName, ToString(fieldInfo.SampleValue)));

            xml += "/></root>";

            var firstDto = CreateDto();
            var secondDto = CreateDto();

            var stream = new XmlTextReader(new MemoryStream(Encoding.UTF8.GetBytes(xml)));

            stream.ReadStartElement();

            firstDto.ReadXml(stream);
            secondDto.ReadXml(stream);

            foreach (var fieldInfo in fieldList)
                Assert.IsTrue(AreEqual(fieldInfo.SampleValue, GetProperty(firstDto, fieldInfo.Name)));

            foreach (var fieldInfo in fieldList)
                Assert.IsTrue(AreEqual(fieldInfo.SampleValue, GetProperty(secondDto, fieldInfo.Name)));
        }


        /// <summary>
        ///   Reading when one required attribute is missing.
        /// </summary>
        [TestMethod]
        public void ReadingFromXmlWhenOneRequiredAttributeIsMissing()
        {
            var fieldList = GetFields().Where(f => f.IsRequired).ToList();

            foreach (var requiredField in fieldList)
            {
                // Encoding the dto
                var xml = string.Format("<{0} ",
                    GetXmlTag());

                // Omitting the required field
                xml = fieldList.Where(f => f.Name != requiredField.Name).Aggregate(xml,
                    (current, fieldInfo) =>
                        current + string.Format("{0}=\"{1}\" ",
                        fieldInfo.XmlName, ToString(fieldInfo.SampleValue)));

                xml += " />";

                var stream = new XmlTextReader(new MemoryStream(Encoding.UTF8.GetBytes(xml)));

                try
                {
                    CreateDto().ReadXml(stream);
                }
                catch (ArgumentException)
                {
                    continue;
                }

                Assert.Fail();
            }
        }


        /// <summary>
        ///   Reading from xml when the xml schema version is not supported.
        /// </summary>
        [TestMethod]
        public void ReadingFromXmlWhenSchemaVersionIsNotSupported()
        {
            var fieldList = GetFields().ToList();

            // Encoding the dto
            var xml = string.Format("<{0} {1}=\"{2}\" ",
                GetXmlTag(),
                DataTransferObject.XmlSchemaVersionTag,
                "500");

            xml = fieldList.Aggregate(xml,
                (current, fieldInfo) =>
                    current + string.Format("{0}=\"{1}\" ",
                    fieldInfo.XmlName, ToString(fieldInfo.SampleValue)));

            xml += "/>";

            var stream = new XmlTextReader(new MemoryStream(Encoding.UTF8.GetBytes(xml)));

            try
            {
                CreateDto().ReadXml(stream);
            }
            catch (ArgumentException)
            {
                return;
            }

            Assert.Fail();
        }


        /// <summary>
        ///   Reading from xml when one of the attributes has wrong format.
        /// </summary>
        [TestMethod]
        public void ReadingFromXmlWhenAttributeHasWrongFormat()
        {
            // Omitting the string fields that always have the right format
            var fieldList = GetFields().Where(f => f.FieldType != DtoFieldType.String).ToList();

            foreach (var currentField in fieldList)
            {
                // Encoding the dto
                var xml = string.Format("<{0} ",
                    GetXmlTag());

                // Omitting the current field
                xml = fieldList.Where(f => f.Name != currentField.Name).Aggregate(xml,
                    (current, fieldInfo) =>
                        current + string.Format("{0}=\"{1}\" ",
                        fieldInfo.XmlName, ToString(fieldInfo.SampleValue)));

                // Adding the current field with wrong format
                xml += string.Format(" {0}=\"{1}\" />", currentField.XmlName, "aa11");

                var stream = new XmlTextReader(new MemoryStream(Encoding.UTF8.GetBytes(xml)));

                try
                {
                    CreateDto().ReadXml(stream);
                }
                catch (ArgumentException)
                {
                    continue;
                }

                Assert.Fail();
            }
        }


        /// <summary>
        ///   Passing null stream when writing xml.
        /// </summary>
        [TestMethod]
        [ExpectedException(typeof(ArgumentNullException))]
        public void PassingNullStreamWhenWritingXml()
        {
            var dto = CreateDto();

            dto.WriteXml(null);
        }


        /// <summary>
        ///   Writing and reading xml.
        /// </summary>
        [TestMethod]
        public void WritingAndReadingXml()
        {
            var firstDto = CreateDto();
            var secondDto = CreateDto();

            var memoryStream = new MemoryStream();
            var writer = new XmlTextWriter(memoryStream, Encoding.UTF8);

            writer.WriteStartElement("root");

            firstDto.WriteXml(writer);
            secondDto.WriteXml(writer);

            writer.WriteEndElement();

            writer.Flush();

            memoryStream.Position = 0;
            var streamReader = new StreamReader(memoryStream);
            var xml = streamReader.ReadToEnd();

            Assert.IsFalse(string.IsNullOrEmpty(xml));

            var firstReadDto = CreateDto();
            var secondReadDto = CreateDto();

            memoryStream.Position = 0;
            var reader = new XmlTextReader(memoryStream);

            reader.ReadStartElement();

            firstReadDto.ReadXml(reader);
            secondReadDto.ReadXml(reader);

            var fieldList = GetFields().ToList();

            foreach (var fieldInfo in fieldList)
                Assert.IsTrue(AreEqual(GetProperty(firstReadDto, fieldInfo.Name),
                    GetProperty(firstDto, fieldInfo.Name)));

            foreach (var fieldInfo in fieldList)
                Assert.IsTrue(AreEqual(GetProperty(secondReadDto, fieldInfo.Name),
                    GetProperty(secondDto, fieldInfo.Name)));
        }


        /// <summary>
        ///   Checking that the xml schema version is serialized.
        /// </summary>
        [TestMethod]
        public void CheckingThatXmlSchemaIsSerialized()
        {
            var dto = CreateDto();

            var memoryStream = new MemoryStream();
            var writer = new XmlTextWriter(memoryStream, Encoding.UTF8);

            dto.WriteXml(writer);

            writer.Flush();

            memoryStream.Position = 0;
            var streamReader = new StreamReader(memoryStream);
            var xml = streamReader.ReadToEnd();

            Assert.IsFalse(string.IsNullOrEmpty(xml));

            var schemaVersionXml = string.Format("{0}=\"{1}\"",
                DataTransferObject.XmlSchemaVersionTag,
                GetCurrentXmlSchemaVersion());

            Assert.IsTrue(xml.Contains(schemaVersionXml));
        }


        /// <summary>
        ///   Checking that null not required fields are not serialized.
        /// </summary>
        [TestMethod]
        public void CheckingThatNullNotRequiredFieldsAreNotSerialized()
        {
            var notRequiredFields = GetFields().Where(f => !f.IsRequired);

            foreach (var fieldInfo in notRequiredFields)
            {
                var dto = CreateDto();

                //Setting the current field value to null
                SetProperty(dto, fieldInfo.Name, null);

                var memoryStream = new MemoryStream();
                var writer = new XmlTextWriter(memoryStream, Encoding.UTF8);

                dto.WriteXml(writer);

                writer.Flush();

                memoryStream.Position = 0;
                var streamReader = new StreamReader(memoryStream);
                var xml = streamReader.ReadToEnd();

                Assert.IsFalse(string.IsNullOrEmpty(xml));

                Assert.IsFalse(xml.Contains(fieldInfo.XmlName));
            }
        }


        /// <summary>
        ///   Copying the properties from the data transfer object.
        /// </summary>
        [TestMethod]
        public void CopyingPropertiesFromDto()
        {
            var dto = CreateDto();

            var copyObject = CopyFrom(dto);

            if (copyObject == null) return;

            foreach (var fieldInfo in GetFields().Where(f => f.IsCopiedFromDto && f.HasTheSameNameAsCorrespondingObject))
                Assert.AreEqual(GetProperty(copyObject, fieldInfo.Name), GetProperty(dto, fieldInfo.Name));
        }


        /// <summary>
        ///   Creating a data transfer object from the corresponding object.
        /// </summary>
        [TestMethod]
        public void CreatingDtoFromCorrespondingObject()
        {
            DataTransferObject dto;
            object initialObject;

            CreateDtoFromObject(out dto, out initialObject);

            if (dto == null) return;

            if (initialObject == null) return;

            foreach (var fieldInfo in GetFields().Where(f => f.HasTheSameNameAsCorrespondingObject))
                Assert.AreEqual(GetProperty(dto, fieldInfo.Name), GetProperty(initialObject, fieldInfo.Name));
        }
    }
}
